package auth

import (
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"regexp"
	"server/components/httputils"
	"server/components/models/ent"
	"server/components/models/ent/user"
	"server/components/repository"
	"strconv"
	"strings"
	"time"

	"github.com/go-chi/chi/v5"
	"golang.org/x/crypto/bcrypt"
)

const (
	AuthCookie = "id"
)

type RegisterAccountRequest struct {
	FirstName string `json:"firstName"`
	LastName  string `json:"lastName"`
	Email     string `json:"email"`
	Password  string `json:"password"`
}

type UpdateAccountRequest struct {
	FirstName *string `json:"firstName,omitempty"`
	LastName  *string `json:"lastName,omitempty"`
	Email     *string `json:"email,omitempty"`
	Password  *string `json:"password,omitempty"`
}

type LoginRequest struct {
	Email    string `json:"email"`
	Password string `json:"password"`
}

type RegisterResponse struct {
	ID        int    `json:"id"`
	FirstName string `json:"firstName"`
	LastName  string `json:"lastName"`
	Email     string `json:"email"`
}

type LoginResponse struct {
	ID int `json:"id"`
}

func (r *RegisterAccountRequest) TrimSpace() RegisterAccountRequest {
	return RegisterAccountRequest{
		FirstName: strings.TrimSpace(r.FirstName),
		LastName:  strings.TrimSpace(r.LastName),
		Email:     strings.TrimSpace(r.Email),
		Password:  strings.TrimSpace(r.Password),
	}
}

func trimSpacePtr(s *string) *string {
	if s == nil {
		return s
	}
	tmp := strings.TrimSpace(*s)
	return &tmp
}

func (r *UpdateAccountRequest) TrimSpace() UpdateAccountRequest {
	return UpdateAccountRequest{
		FirstName: trimSpacePtr(r.FirstName),
		LastName:  trimSpacePtr(r.LastName),
		Email:     trimSpacePtr(r.Email),
		Password:  trimSpacePtr(r.Password),
	}
}

func (r *UpdateAccountRequest) hasEmpty() bool {
	for _, it := range []*string{r.FirstName, r.LastName, r.Email, r.Password} {
		if it != nil && *it == "" {
			return true
		}
	}
	return false
}

var (
	emailRegexp = regexp.MustCompile(`(?m)^(((((((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?([A-Za-z0-9!#-'*+\/=?^_\x60{|}~-])+((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?)|(((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?"((\s? +)?(([!#-[\]-~])|(\\([ -~]|\s))))*(\s? +)?"))?)?(((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?<(((((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?(([A-Za-z0-9!#-'*+\/=?^_\x60{|}~-])+(\.([A-Za-z0-9!#-'*+\/=?^_\x60{|}~-])+)*)((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?)|(((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?"((\s? +)?(([!#-[\]-~])|(\\([ -~]|\s))))*(\s? +)?"))@((((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?(([A-Za-z0-9!#-'*+\/=?^_\x60{|}~-])+(\.([A-Za-z0-9!#-'*+\/=?^_\x60{|}~-])+)*)((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?)|(((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?\[((\s? +)?([!-Z^-~]))*(\s? +)?\]((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?)))>((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?))|(((((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?(([A-Za-z0-9!#-'*+\/=?^_\x60{|}~-])+(\.([A-Za-z0-9!#-'*+\/=?^_\x60{|}~-])+)*)((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?)|(((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?"((\s? +)?(([!#-[\]-~])|(\\([ -~]|\s))))*(\s? +)?"))@((((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?(([A-Za-z0-9!#-'*+\/=?^_\x60{|}~-])+(\.([A-Za-z0-9!#-'*+\/=?^_\x60{|}~-])+)*)((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?)|(((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?\[((\s? +)?([!-Z^-~]))*(\s? +)?\]((((\s? +)?(\(((\s? +)?(([!-'*-[\]-~]*)|(\\([ -~]|\s))))*(\s? +)?\)))(\s? +)?)|(\s? +))?))))$`)
)

func isAuthorized(r *http.Request, email string) (int, bool) {
	c, err := r.Cookie(AuthCookie)
	if err != nil {
		return -1, false
	}
	id, err := strconv.ParseInt(c.Value, 10, 32)
	if err != nil {
		return -1, false
	}
	if user, err := repository.EntClient().User.Get(r.Context(), int(id)); err == nil {
		return user.ID, user.Email == email
	}
	return -1, false
}

func VerifyHandler(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		c, err := r.Cookie(AuthCookie)
		if err != nil {
			http.Error(w, "Неверные авторизационные данные", http.StatusUnauthorized)
			return
		}
		id, err := strconv.ParseInt(c.Value, 10, 32)
		if err != nil {
			http.Error(w, fmt.Sprintf("Неверные авторизационные данные, ожидается формат int32 %v", c.Value), http.StatusUnauthorized)
			return
		}
		user, err := repository.EntClient().User.Get(r.Context(), int(id))
		if ent.IsNotFound(err) {
			http.Error(w, fmt.Sprintf("Аккаунт с таким id = %v не найден", id), http.StatusNotFound)
			return
		}
		next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), httputils.UserIDCtxKey, user.ID)))
	})
}

// @Summary      Регистрация учетной записи
// @Description  Регистрация учетной записи
// @Tags         accounts
// @Param        data	body      RegisterAccountRequest  true  "Данные учетной записи для регистрации"
// @Produce      json
// @Success      201  {object}  ent.User
// @Failure      400  {string}  error "Некорректные данные"
// @Failure      403  {string}  error "Запрос от авторизованного аккаунта"
// @Failure      409  {string}  error "Аккаунт с таким email уже существует"
// @Router       /registration [post]
func RegisterAccount(w http.ResponseWriter, r *http.Request) {
	_, err := r.Cookie(AuthCookie)
	if err == nil {
		http.Error(w, "Запрос от авторизированного аккаунта", http.StatusForbidden)
		return
	}
	req := RegisterAccountRequest{}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, fmt.Sprintf("Некорректный JSON формат %v", err), http.StatusBadRequest)
		return
	}
	req = req.TrimSpace()
	if req.Email == "" || req.FirstName == "" || req.LastName == "" || req.Password == "" {
		http.Error(w, "Пустые поля не допускаются", http.StatusBadRequest)
		return
	}
	if !emailRegexp.Match([]byte(req.Email)) {
		http.Error(w, "Некорректный формат email", http.StatusBadRequest)
		return
	}
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
	if err != nil {
		http.Error(w, "Невозможно создать хэш от пароля", http.StatusInternalServerError)
		return
	}
	user, err := repository.EntClient().
		User.Create().
		SetEmail(req.Email).
		SetFirstName(req.FirstName).
		SetLastName(req.LastName).
		SetPassword(hashedPassword).Save(r.Context())
	if ent.IsConstraintError(err) {
		http.Error(w, "Аккаунт с таким email уже существует", http.StatusConflict)
		return
	} else if err != nil {
		http.Error(w, "Ошибка создания аккаунгта", http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusCreated)
	httputils.WriteJson(w, user)
}

func RegisterAccountRaw(ctx context.Context, req RegisterAccountRequest) error {
	req = req.TrimSpace()
	if req.Email == "" || req.FirstName == "" || req.LastName == "" || req.Password == "" {
		return fmt.Errorf("пустые поля не допускаются")
	}
	if !emailRegexp.Match([]byte(req.Email)) {
		return fmt.Errorf("не корректный email")
	}
	hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
	if err != nil {
		return err
	}
	_, err = repository.EntClient().
		User.Create().
		SetEmail(req.Email).
		SetFirstName(req.FirstName).
		SetLastName(req.LastName).
		SetPassword(hashedPassword).Save(ctx)
	if ent.IsConstraintError(err) {
		return fmt.Errorf("аккаунт с таким email уже существует")
	} else if err != nil {
		return err
	}
	return nil
}

// @Summary      Вход в систему
// @Description  Вход в систему. При успешном входе, id пользователя возвращается в Cookie "id"
// @Tags         accounts
// @Param        data	body      LoginRequest  true  "Данные учетной записи для входа"
// @Produce      json
// @Success      200  {object}  LoginResponse
// @Failure      401  {string}  error "Логин или пароль не верны"
// @Router       /login [post]
func Login(w http.ResponseWriter, r *http.Request) {
	req := LoginRequest{}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, fmt.Sprintf("Некорректный JSON формат %v", err), http.StatusBadRequest)
		return
	}
	if id, ok := isAuthorized(r, req.Email); ok {
		httputils.WriteJson(w, LoginResponse{id})
		return
	}
	user, err := repository.EntClient().
		User.Query().
		Where(user.Email(req.Email)).
		Only(r.Context())
	if ent.IsNotFound(err) {
		http.Error(w, fmt.Sprintf("Аккаунт с таким email = %v не найден", req.Email), http.StatusUnauthorized)
		return
	}
	http.SetCookie(w, &http.Cookie{
		HttpOnly: true,
		Expires:  time.Now().Add(24 * time.Hour),
		SameSite: http.SameSiteLaxMode,
		Secure:   false,
		Name:     AuthCookie,
		Value:    strconv.FormatInt(int64(user.ID), 10),
	})
	httputils.WriteJson(w, LoginResponse{user.ID})
}

// @Summary      Получение учетной записи пользователя по id
// @Description  Получение учетной записи пользователя по id
// @Tags         accounts
// @Produce      json
// @Param        accountId   path      int  true  "accountId"
// @Success      200  {object}  ent.User
// @Failure      400  {string}  error "Некорректные данные"
// @Failure      401  {string}  error "Пользователь не авторизирован"
// @Failure      404  {string}  error "Пользователь не найден"
// @Router       /accounts/{accountId} [get]
func GetAccount(w http.ResponseWriter, r *http.Request) {
	val := chi.URLParam(r, "accountId")
	id, err := strconv.ParseInt(val, 10, 32)
	if err != nil {
		http.Error(w, fmt.Sprintf("Неверный формат accountId, ожидается формат int32 вместо %v", val), http.StatusUnauthorized)
		return
	}
	user, err := repository.EntClient().User.Get(r.Context(), int(id))
	if ent.IsNotFound(err) {
		http.Error(w, fmt.Sprintf("Пользователь %v не найден", id), http.StatusNotFound)
		return
	}
	httputils.WriteJson(w, user)
}

// @Summary      Обновление учетной записи пользователя по id
// @Description  Обновление учетной записи пользователя по id
// @Tags         accounts
// @Produce      json
// @Param        accountId   path      int  true  "accountId"
// @Param        data	body      UpdateAccountRequest  true  "Данные для обновления учетной записи. Обновляются только переданные поля"
// @Success      200  {object}  ent.User
// @Failure      400  {string}  error "Некорректные данные"
// @Failure      401  {string}  error "Пользователь не авторизирован"
// @Failure      403  {string}  error "Обновление не своего аккаунта или аккаунт не найден"
// @Failure      409  {string}  error "Аккаунт с таким email уже существует"
// @Router       /accounts/{accountId} [put]
func UpdateAccount(w http.ResponseWriter, r *http.Request) {
	userID := r.Context().Value(httputils.UserIDCtxKey).(int)
	accountId, err := strconv.ParseInt(chi.URLParam(r, "accountId"), 10, 32)
	if err != nil || accountId < 0 {
		http.Error(w, "Некорректный accountId", http.StatusBadRequest)
		return
	}
	if userID != int(accountId) {
		http.Error(w, "Обновление не своего аккаунта", http.StatusForbidden)
		return
	}
	req := UpdateAccountRequest{}
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, fmt.Sprintf("Некорректный JSON формат %v", err), http.StatusBadRequest)
		return
	}
	req = req.TrimSpace()
	if req.hasEmpty() {
		http.Error(w, "Пустые поля не допускаются", http.StatusBadRequest)
		return
	}
	if req.Email != nil {
		if !emailRegexp.Match([]byte(*req.Email)) {
			http.Error(w, "Некорректный формат email", http.StatusBadRequest)
			return
		}
	}
	update := repository.EntClient().User.UpdateOneID(int(accountId))
	if req.LastName != nil {
		update = update.SetLastName(*req.LastName)
	}
	if req.FirstName != nil {
		update = update.SetFirstName(*req.FirstName)
	}
	if req.Password != nil {
		hashedPassword, err := bcrypt.GenerateFromPassword([]byte(*req.Password), bcrypt.DefaultCost)
		if err != nil {
			http.Error(w, "Невозможно создать хэш от пароля", http.StatusInternalServerError)
			return
		}
		update = update.SetPassword(hashedPassword)
	}
	if req.Email != nil {
		update = update.SetEmail(*req.Email)
	}
	user, err := update.Save(r.Context())
	if ent.IsConstraintError(err) {
		http.Error(w, "Аккаунт с таким email уже существует", http.StatusConflict)
		return
	} else if err != nil {
		http.Error(w, "Ошибка обновления аккаунта", http.StatusInternalServerError)
		return
	}
	httputils.WriteJson(w, user)
}

// @Summary      Удаление учетной записи пользователя по id
// @Description  Удаление учетной записи пользователя по id
// @Tags         accounts
// @Produce      json
// @Param        accountId   path      int  true  "accountId"
// @Success      200  {object}  httputils.EmptyResponse
// @Failure      400  {string}  error "Некорректные данные"
// @Failure      401  {string}  error "Пользователь не авторизирован"
// @Failure      403  {string}  error "Удаление не своего аккаунта или аккаунт не найден"
// @Router       /accounts/{accountId} [delete]
func DeleteAccount(w http.ResponseWriter, r *http.Request) {
	userID := r.Context().Value(httputils.UserIDCtxKey).(int)
	accountId, err := strconv.ParseInt(chi.URLParam(r, "accountId"), 10, 32)
	if err != nil || accountId < 0 {
		http.Error(w, "Некорректный accountId", http.StatusBadRequest)
		return
	}
	if userID != int(accountId) {
		http.Error(w, "Удаление не своего аккаунта", http.StatusForbidden)
		return
	}
	if err := repository.EntClient().User.DeleteOneID(int(accountId)).Exec(r.Context()); err != nil {
		if ent.IsNotFound(err) {
			http.Error(w, "Аккаунт с таким id не найден", http.StatusForbidden)
			return
		} else {
			http.Error(w, "Ошибка удаления аккаунта: "+err.Error(), http.StatusInternalServerError)
			return
		}
	}
	httputils.WriteJson(w, struct{}{})
}
